package core

import (
	"bufio"
	"fmt"
	"os"
	"path/filepath"
	"sync"
	"time"

	"github.com/jmoiron/sqlx"
	"github.com/ledgerwatch/erigon/core/state"
	"github.com/ledgerwatch/log/v3"
	_ "github.com/lib/pq"
)

type ProcessMetrics struct {
	Number uint64 `db:"number"`

	ChaindbStorage float64 `db:"chaindb_storage"`
}

var db *sqlx.DB
var dbOnce sync.Once
var syncRedisOnce sync.Once
var archiveTableName = "erigon_archive_statistics_10w"
var StaticDirPath = "/data/kyh/2.erigon_non_sync"
var lastStaticBlockNumber uint64 = 0
var RedisConfig = state.NewDefaultRedisConfig("erigon")

var historyTableName string

var EnableMetrics = true

const (
	genChangeSetType = "gen_change_set"
	writeChangeSet   = "write_change_set"
	genHisIdxTable   = "gen_history_index_table"
	writeHisIdxTable = "write_history_index_table"
)

type CreateTableMetrics struct {
	from, to uint64
	TimeCost time.Duration
}

var StuckBlockNum uint64 = 1000000

const (
	ReportBatch = 100000
)

func UpdateMetrics(m *ProcessMetrics) (err error) {
	dbOnce.Do(func() {
		db, err = sqlx.Connect("postgres",
			"host=192.168.3.151 port=8888 dbname=experiment user=experiment password=experiment@blockSec888 sslmode=disable",
		)
	})
	if err != nil {
		return
	}
	_, err = db.NamedExec("insert into "+archiveTableName+` ("number", "chaindb_storage") values (:number, :chaindb_storage)`, m)
	return err
}

func SetHistoryTableName(tableName string) {
	historyTableName = tableName
}

var genChangeSetTableTimeCost time.Duration = 0
var lastStatGenChangeTableBlockNumber uint64 = 0

func HookGenChangeSetTable(blockNumber uint64, timeCost time.Duration) {
	genChangeSetTableTimeCost += timeCost
	if blockNumber%ReportBatch == 0 {
		m := &CreateTableMetrics{
			from:     lastStatGenChangeTableBlockNumber,
			to:       blockNumber,
			TimeCost: genChangeSetTableTimeCost,
		}
		err := UpdateCreateTableMetrics(m, "Gen Change Set", fmt.Sprintf("./%s.csv", genChangeSetType))
		if err != nil {
			log.Error("UpdateCreateTableMetrics failed", "err", err)
		}
		genChangeSetTableTimeCost = 0
		lastStatGenChangeTableBlockNumber = blockNumber
	}
}

var writeChangeSetTableTimeCost time.Duration = 0
var lastStatWriteChangeTableBlockNumber uint64 = 0

func HookWriteChangeSetTable(blockNumber uint64, timeCost time.Duration) {
	writeChangeSetTableTimeCost += timeCost
	if blockNumber%ReportBatch == 0 {
		m := &CreateTableMetrics{
			from:     lastStatWriteChangeTableBlockNumber,
			to:       blockNumber,
			TimeCost: writeChangeSetTableTimeCost,
		}
		err := UpdateCreateTableMetrics(m, "Write Change Set", fmt.Sprintf("./%s.csv", writeChangeSet))
		if err != nil {
			log.Error("UpdateCreateTableMetrics failed", "err", err)
		}
		writeChangeSetTableTimeCost = 0
		lastStatWriteChangeTableBlockNumber = blockNumber
	}
}

var lastStatGenHisIdxTableBlockNumber uint64 = 0
var genHisIdxTableTimeCost time.Duration = 0

func HookGenHistroyIndexTable(blockNumber uint64, timeCost time.Duration) {
	genHisIdxTableTimeCost += timeCost
	if blockNumber > lastStatGenHisIdxTableBlockNumber && (blockNumber%ReportBatch == 0 || blockNumber-lastStatGenHisIdxTableBlockNumber >= 100000) {
		m := &CreateTableMetrics{
			from:     lastStatGenHisIdxTableBlockNumber,
			to:       blockNumber,
			TimeCost: genHisIdxTableTimeCost,
		}
		err := UpdateCreateTableMetrics(m, "Gen "+historyTableName, fmt.Sprintf("./%s_%s.csv", genHisIdxTable, historyTableName))
		if err != nil {
			log.Error("UpdateCreateTableMetrics failed", "err", err)
		}
		lastStatGenHisIdxTableBlockNumber = blockNumber
		genHisIdxTableTimeCost = 0
	}
}

func HookWriteHistoryTable(from, to uint64, timeCost time.Duration) {
	m := &CreateTableMetrics{
		from:     from,
		to:       to,
		TimeCost: timeCost,
	}
	err := UpdateCreateTableMetrics(m, "Write Table "+historyTableName, fmt.Sprintf("./%s_%s.csv", writeHisIdxTable, historyTableName))
	if err != nil {
		log.Error("UpdateCreateTableMetrics failed", "err", err)
	}
}

func UpdateCreateTableMetrics(m *CreateTableMetrics, prefix string, fileName string) error {
	log.Info(prefix, "from", m.from, "to", m.to, "timeCost", m.TimeCost)
	file, err := os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	if err != nil {
		return err
	}
	defer file.Close()
	writer := bufio.NewWriter(file)
	defer writer.Flush()
	_, err = writer.WriteString(fmt.Sprintf("%v,%v,%v\n", m.from, m.to, m.TimeCost))
	if err != nil {
		return err
	}
	return nil
}

func ResetTableMetrics() {
	genChangeSetTableTimeCost = 0
	writeChangeSetTableTimeCost = 0
	genHisIdxTableTimeCost = 0

	lastStatGenChangeTableBlockNumber = 0
	lastStatWriteChangeTableBlockNumber = 0
	lastStatGenHisIdxTableBlockNumber = 0
}

func HookStatistic(currentBlock uint64) error {
	updateStuckNumFunc := func() error {
		stuckNum, err := RedisConfig.LoadStuckBlockNum()
		if err != nil {
			log.Error("LoadStuckBlockNum failed", "err", err)
			stuckNum = StuckBlockNum
		}
		StuckBlockNum = stuckNum
		return err
	}

	syncRedisOnce.Do(func() {
		loadNum, err := RedisConfig.LoadStatisticBlockNum()
		if err != nil {
			log.Error("LoadStatisticBlockNum failed", "err", err)
			loadNum = 0
		}
		lastStaticBlockNumber = loadNum

	})

	if currentBlock >= StuckBlockNum {
		log.Info("Begin Stuck", "currentBlock", currentBlock, "StuckBlockNum", StuckBlockNum)
		for currentBlock >= StuckBlockNum {
			time.Sleep(10 * time.Second)
			updateStuckNumFunc()
		}
	}

	if currentBlock < lastStaticBlockNumber {
		return nil
	}

	if currentBlock-lastStaticBlockNumber >= ReportBatch {
		m := &ProcessMetrics{
			Number:         currentBlock,
			ChaindbStorage: DirSizeGB(StaticDirPath),
		}
		updateStuckNumFunc()

		log.Info("Statistic", "currentBlock", currentBlock, "lastStaticBlockNumber", lastStaticBlockNumber, "chaindbStorage", m.ChaindbStorage)
		err := UpdateMetrics(m)
		if err != nil {
			log.Error("UpdateMetrics failed", "err", err)
		}
		lastStaticBlockNumber = currentBlock
		err = RedisConfig.StoreStatisticBlockNum(currentBlock)
		if err != nil {
			log.Error("StoreStatisticBlockNum failed", "err", err)
		}
		return nil
	}
	return nil
}

func DirSizeGB(path string) float64 {
	var size int64
	filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
		if err != nil {
			log.Warn("read dirsize failed", "err", err)
			return nil
		}
		if info == nil {
			log.Warn("read dirsize failed", "err", "file info is nil")
			return nil
		}
		if !info.IsDir() {
			size += info.Size()
		}
		return nil
	})
	return float64(size) / 1024.0 / 1024.0 / 1024.0
}

type InsertMetrics struct {
	Exec          time.Duration
	TrieRead      time.Duration
	StateDbCommit time.Duration
	TrieHash      time.Duration
	TrieUpdate    time.Duration
	TrieCommit    time.Duration
	SnapCommit    time.Duration
	TrieDBUpdate  time.Duration
	BlockWrite    time.Duration
}
